/* Copyright 2012 Mozilla Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @module pdfjsLib
 */
import {
    createPromiseCapability,
    getVerbosityLevel,
    shadow,
    warn,
    AnnotationMode,
    RenderingIntentFlag,
    AbortException
} from "../shared/util.js";
import {MessageHandler} from "../shared/message_handler.js";
import {WorkerMessageHandler} from "../core/worker.js";
import { XfaText } from "./xfa_text.js";
import {
    deprecated,
    DOMCanvasFactory,
    DOMCMapReaderFactory,
    DOMStandardFontDataFactory,
    isDataScheme,
    loadScript,
    PageViewport,
    RenderingCancelledException,
    StatTimer,
} from "./display_utils.js";
import { OptionalContentConfig } from "./optional_content_config.js";
import { CanvasGraphics } from "./canvas.js";
import { FontFaceObject, FontLoader } from "./font_loader.js";
import {deepCopy} from '../deepCopyAll.js';

const DefaultCanvasFactory = DOMCanvasFactory;
const DefaultCMapReaderFactory = DOMCMapReaderFactory;
const DefaultStandardFontDataFactory =DOMStandardFontDataFactory;

const PDFWorkerUtil = {
    isWorkerDisabled: false,
    fallbackWorkerSrc: null,
    fakeWorkerId: 0,
};


class PDFWorker {
    static get _workerPorts() {
        return shadow(this, "_workerPorts", new WeakMap());
    }

    constructor({
                    name = null,
                    port = null,
                    verbosity = getVerbosityLevel(),
                } = {}) {
        if (port && PDFWorker._workerPorts.has(port)) {
            throw new Error("Cannot use more than one PDFWorker per port.");
        }

        this.name = name;
        this.destroyed = false;
        this.verbosity = verbosity;

        this._readyCapability = createPromiseCapability();
        this._port = null;
        this._webWorker = null;
        this._messageHandler = null;
        this._initialize();
    }

    get promise() {
        return this._readyCapability.promise;
    }

    get port() {
        return this._port;
    }

    get messageHandler() {
        return this._messageHandler;
    }

    _initialize() {
        this._setupFakeWorker();
    }

    _setupFakeWorker() {
        if (!PDFWorkerUtil.isWorkerDisabled) {
            warn("Setting up fake worker.");
            PDFWorkerUtil.isWorkerDisabled = true;
        }
        //var workerMessageHandler = shadow(this, "_setupFakeWorkerGlobal", new WorkerMessageHandler());
        //PDFWorker._setupFakeWorkerGlobal
        //    .then(WorkerMessageHandler => {
        if (this.destroyed) {
            this._readyCapability.reject(new Error("Worker was destroyed"));
            return;
        }
        const port = new LoopbackPort();
        this._port = port;

        // All fake workers use the same port, making id unique.
        const id = `fake${PDFWorkerUtil.fakeWorkerId++}`;

        // If the main thread is our worker, setup the handling for the
        // messages -- the main thread sends to it self.
        const workerHandler = new MessageHandler(id + "_worker", id, port);
        WorkerMessageHandler.setup(workerHandler, port);

        const messageHandler = new MessageHandler(id, id + "_worker", port);
        this._messageHandler = messageHandler;
        this._readyCapability.resolve();
        // Send global setting, e.g. verbosity level.
        messageHandler.send("configure", {
            verbosity: this.verbosity,
        });
        // })
        //  .catch(reason => {
        //      this._readyCapability.reject(
        //         new Error(`Setting up fake worker failed: "${reason.message}".`)
        //     );
        // });
    }

    static get _setupFakeWorkerGlobal() {
        return shadow(this, "_setupFakeWorkerGlobal", new WorkerMessageHandler());
    }
}

class LoopbackPort {
    constructor() {
        this._listeners = [];
        this._deferred = Promise.resolve();
    }

    postMessage(obj, transfers) {
        const event = {
            data: deepCopy(obj),
        };

        this._deferred.then(() => {
            for (const listener of this._listeners) {
                listener.call(this, event);
            }
        });
    }

    addEventListener(name, listener) {
        this._listeners.push(listener);
    }

    removeEventListener(name, listener) {
        const i = this._listeners.indexOf(listener);
        this._listeners.splice(i, 1);
    }

    terminate() {
        this._listeners.length = 0;
    }
}

class PDFDocumentLoadingTask {
    static get idCounters() {
        return shadow(this, "idCounters", { doc: 0 });
    }

    constructor() {
        this._capability = createPromiseCapability();
        this._transport = null;
        this._worker = null;

        /**
         * Unique identifier for the document loading task.
         * @type {string}
         */
        this.docId = `d${PDFDocumentLoadingTask.idCounters.doc++}`;

        /**
         * Whether the loading task is destroyed or not.
         * @type {boolean}
         */
        this.destroyed = false;

        /**
         * Callback to request a password if a wrong or no password was provided.
         * The callback receives two parameters: a function that should be called
         * with the new password, and a reason (see {@link PasswordResponses}).
         * @type {function}
         */
        this.onPassword = null;

        /**
         * Callback to be able to monitor the loading progress of the PDF file
         * (necessary to implement e.g. a loading bar).
         * The callback receives an {@link OnProgressParameters} argument.
         * @type {function}
         */
        this.onProgress = null;

        /**
         * Callback for when an unsupported feature is used in the PDF document.
         * The callback receives an {@link UNSUPPORTED_FEATURES} argument.
         * @type {function}
         */
        this.onUnsupportedFeature = null;
    }

    /**
     * Promise for document loading task completion.
     * @type {Promise<PDFDocumentProxy>}
     */
    get promise() {
        return this._capability.promise;
    }

    /**
     * Abort all network requests and destroy the worker.
     * @returns {Promise<void>} A promise that is resolved when destruction is
     *   completed.
     */
    async destroy() {
        this.destroyed = true;
        await this._transport?.destroy();

        this._transport = null;
        if (this._worker) {
            this._worker.destroy();
            this._worker = null;
        }
    }
}

function getDocument(src) {
    const task = new PDFDocumentLoadingTask();
    var worker = new PDFWorker();
    task._worker = worker;
    const docId = task.docId;
    worker.promise
        .then(function () {
            if (task.destroyed) {
                throw new Error("Loading aborted");
            }

            const workerIdPromise = _fetchDocument(worker, src, docId);
            return Promise.all([workerIdPromise]).then(
                function ([workerId]) {
                    console.log("sreeni pdf2:----------:" + workerId);
                    if (task.destroyed) {
                        throw new Error("Loading aborted");
                    }

                    const messageHandler = new MessageHandler(
                        docId,
                        workerId,
                        worker.port
                    );
                    //TODO sreeni handle transport
                    const transport = new WorkerTransport(
                        messageHandler,
                        task,
                        null,
                        {"ownerDocument":null,"pdfBug":null}
                    );
                    task._transport = transport;
                    messageHandler.send("Ready", null);
                }
            );
        });
    return task;
}

async function _fetchDocument(worker, source, docId) {
    if (worker.destroyed) {
        throw new Error("Worker was destroyed");
    }
    const workerId = await worker._messageHandler.sendWithPromise("GetDocRequest", {
        "apiVersion": "2.13.167",
        "cMapUrl": undefined,
        "disableFontFace": false,
        "docBaseUrl": null,
        "docId": docId,
        "enableXfa": false,
        "fontExtraProperties": false,
        "ignoreErrors": true,
        "isEvalSupported": true,
        "maxImageSize": -1,
        "source": {
            data: source,
            url: undefined,
            password: undefined,
            disableAutoFetch: false,
            rangeChunkSize: 65536,
            length: undefined,
        },
        "standardFontDataUrl": undefined,
        "useSystemFonts": true
    });
    if (worker.destroyed) {
        throw new Error("Worker was destroyed");
    }
    return workerId;
}

class WorkerTransport {
    #docStats = null;

    #pageCache = new Map();

    #pagePromises = new Map();

    #metadataPromise = null;

    constructor(messageHandler, loadingTask, networkStream, params) {
        this.messageHandler = messageHandler;
        this.loadingTask = loadingTask;
        this.commonObjs = new PDFObjects();
        this.fontLoader = new FontLoader({
            docId: loadingTask.docId,
            onUnsupportedFeature: this._onUnsupportedFeature.bind(this),
            ownerDocument: params.ownerDocument,
            styleElement: params.styleElement,
        });
        this._params = params;
        //TODO sreeni
        /*if (!params.useWorkerFetch) {
            this.CMapReaderFactory = new params.CMapReaderFactory({
                baseUrl: params.cMapUrl,
                isCompressed: params.cMapPacked,
            });
            this.StandardFontDataFactory = new params.StandardFontDataFactory({
                baseUrl: params.standardFontDataUrl,
            });
        }*/

        this.destroyed = false;
        this.destroyCapability = null;
        this._passwordCapability = null;

        this._networkStream = networkStream;
        this._fullReader = null;
        this._lastProgress = null;
        this.downloadInfoCapability = createPromiseCapability();

        this.setupMessageHandler();
    }

    get annotationStorage() {
        return shadow(this, "annotationStorage", new AnnotationStorage());
    }

    get stats() {
        return this.#docStats;
    }

    getRenderingIntent(
        intent,
        annotationMode = AnnotationMode.ENABLE,
        isOpList = false
    ) {
        let renderingIntent = RenderingIntentFlag.DISPLAY; // Default value.
        let lastModified = "";

        switch (intent) {
            case "any":
                renderingIntent = RenderingIntentFlag.ANY;
                break;
            case "display":
                break;
            case "print":
                renderingIntent = RenderingIntentFlag.PRINT;
                break;
            default:
                warn(`getRenderingIntent - invalid intent: ${intent}`);
        }

        switch (annotationMode) {
            case AnnotationMode.DISABLE:
                renderingIntent += RenderingIntentFlag.ANNOTATIONS_DISABLE;
                break;
            case AnnotationMode.ENABLE:
                break;
            case AnnotationMode.ENABLE_FORMS:
                renderingIntent += RenderingIntentFlag.ANNOTATIONS_FORMS;
                break;
            case AnnotationMode.ENABLE_STORAGE:
                renderingIntent += RenderingIntentFlag.ANNOTATIONS_STORAGE;

                lastModified = this.annotationStorage.lastModified;
                break;
            default:
                warn(`getRenderingIntent - invalid annotationMode: ${annotationMode}`);
        }

        if (isOpList) {
            renderingIntent += RenderingIntentFlag.OPLIST;
        }

        return {
            renderingIntent,
            cacheKey: `${renderingIntent}_${lastModified}`,
        };
    }

    destroy() {
        if (this.destroyCapability) {
            return this.destroyCapability.promise;
        }

        this.destroyed = true;
        this.destroyCapability = createPromiseCapability();

        if (this._passwordCapability) {
            this._passwordCapability.reject(
                new Error("Worker was destroyed during onPassword callback")
            );
        }

        const waitOn = [];
        // We need to wait for all renderings to be completed, e.g.
        // timeout/rAF can take a long time.
        for (const page of this.#pageCache.values()) {
            waitOn.push(page._destroy());
        }
        this.#pageCache.clear();
        this.#pagePromises.clear();
        // Allow `AnnotationStorage`-related clean-up when destroying the document.
        if (this.hasOwnProperty("annotationStorage")) {
            this.annotationStorage.resetModified();
        }
        // We also need to wait for the worker to finish its long running tasks.
        const terminated = this.messageHandler.sendWithPromise("Terminate", null);
        waitOn.push(terminated);

        Promise.all(waitOn).then(() => {
            this.commonObjs.clear();
            this.fontLoader.clear();
            this.#metadataPromise = null;
            this._getFieldObjectsPromise = null;
            this._hasJSActionsPromise = null;

            if (this._networkStream) {
                this._networkStream.cancelAllRequests(
                    new AbortException("Worker was terminated.")
                );
            }

            if (this.messageHandler) {
                this.messageHandler.destroy();
                this.messageHandler = null;
            }
            this.destroyCapability.resolve();
        }, this.destroyCapability.reject);
        return this.destroyCapability.promise;
    }

    setupMessageHandler() {
        const { messageHandler, loadingTask } = this;

        messageHandler.on("GetReader", (data, sink) => {
            assert(
                this._networkStream,
                "GetReader - no `IPDFStream` instance available."
            );
            this._fullReader = this._networkStream.getFullReader();
            this._fullReader.onProgress = evt => {
                this._lastProgress = {
                    loaded: evt.loaded,
                    total: evt.total,
                };
            };
            sink.onPull = () => {
                this._fullReader
                    .read()
                    .then(function ({ value, done }) {
                        if (done) {
                            sink.close();
                            return;
                        }
                        assert(
                        isArrayBuffer(value),
                            "GetReader - expected an ArrayBuffer."
                        );
                        // Enqueue data chunk into sink, and transfer it
                        // to other side as `Transferable` object.
                        sink.enqueue(new Uint8Array(value), 1, [value]);
                    })
                    .catch(reason => {
                        sink.error(reason);
                    });
            };

            sink.onCancel = reason => {
                this._fullReader.cancel(reason);

                sink.ready.catch(readyReason => {
                    if (this.destroyed) {
                        return; // Ignore any pending requests if the worker was terminated.
                    }
                    throw readyReason;
                });
            };
        });

        messageHandler.on("ReaderHeadersReady", data => {
            const headersCapability = createPromiseCapability();
            const fullReader = this._fullReader;
            fullReader.headersReady.then(() => {
                // If stream or range are disabled, it's our only way to report
                // loading progress.
                if (!fullReader.isStreamingSupported || !fullReader.isRangeSupported) {
                    if (this._lastProgress) {
                        loadingTask.onProgress?.(this._lastProgress);
                    }
                    fullReader.onProgress = evt => {
                        loadingTask.onProgress?.({
                            loaded: evt.loaded,
                            total: evt.total,
                        });
                    };
                }

                headersCapability.resolve({
                    isStreamingSupported: fullReader.isStreamingSupported,
                    isRangeSupported: fullReader.isRangeSupported,
                    contentLength: fullReader.contentLength,
                });
            }, headersCapability.reject);

            return headersCapability.promise;
        });

        messageHandler.on("GetRangeReader", (data, sink) => {
            assert(
                this._networkStream,
                "GetRangeReader - no `IPDFStream` instance available."
            );
            const rangeReader = this._networkStream.getRangeReader(
                data.begin,
                data.end
            );

            // When streaming is enabled, it's possible that the data requested here
            // has already been fetched via the `_fullRequestReader` implementation.
            // However, given that the PDF data is loaded asynchronously on the
            // main-thread and then sent via `postMessage` to the worker-thread,
            // it may not have been available during parsing (hence the attempt to
            // use range requests here).
            //
            // To avoid wasting time and resources here, we'll thus *not* dispatch
            // range requests if the data was already loaded but has not been sent to
            // the worker-thread yet (which will happen via the `_fullRequestReader`).
            if (!rangeReader) {
                sink.close();
                return;
            }

            sink.onPull = () => {
                rangeReader
                    .read()
                    .then(function ({ value, done }) {
                        if (done) {
                            sink.close();
                            return;
                        }
                        assert(
                        isArrayBuffer(value),
                            "GetRangeReader - expected an ArrayBuffer."
                        );
                        sink.enqueue(new Uint8Array(value), 1, [value]);
                    })
                    .catch(reason => {
                        sink.error(reason);
                    });
            };

            sink.onCancel = reason => {
                rangeReader.cancel(reason);

                sink.ready.catch(readyReason => {
                    if (this.destroyed) {
                        return; // Ignore any pending requests if the worker was terminated.
                    }
                    throw readyReason;
                });
            };
        });

        messageHandler.on("GetDoc", ({ pdfInfo }) => {
            this._numPages = pdfInfo.numPages;
            this._htmlForXfa = pdfInfo.htmlForXfa;
            delete pdfInfo.htmlForXfa;
            loadingTask._capability.resolve(new PDFDocumentProxy(pdfInfo, this));
        });

        messageHandler.on("DocException", function (ex) {
            let reason;
            switch (ex.name) {
                case "PasswordException":
                    reason = new PasswordException(ex.message, ex.code);
                    break;
                case "InvalidPDFException":
                    reason = new InvalidPDFException(ex.message);
                    break;
                case "MissingPDFException":
                    reason = new MissingPDFException(ex.message);
                    break;
                case "UnexpectedResponseException":
                    reason = new UnexpectedResponseException(ex.message, ex.status);
                    break;
                case "UnknownErrorException":
                    reason = new UnknownErrorException(ex.message, ex.details);
                    break;
                default:
                    unreachable("DocException - expected a valid Error.");
            }
            loadingTask._capability.reject(reason);
        });

        messageHandler.on("PasswordRequest", exception => {
            this._passwordCapability = createPromiseCapability();

            if (loadingTask.onPassword) {
                const updatePassword = password => {
                    if (password instanceof Error) {
                        this._passwordCapability.reject(password);
                    } else {
                        this._passwordCapability.resolve({ password });
                    }
                };
                try {
                    loadingTask.onPassword(updatePassword, exception.code);
                } catch (ex) {
                    this._passwordCapability.reject(ex);
                }
            } else {
                this._passwordCapability.reject(
                    new PasswordException(exception.message, exception.code)
                );
            }
            return this._passwordCapability.promise;
        });

        messageHandler.on("DataLoaded", data => {
            // For consistency: Ensure that progress is always reported when the
            // entire PDF file has been loaded, regardless of how it was fetched.
            loadingTask.onProgress?.({
                loaded: data.length,
                total: data.length,
            });

            this.downloadInfoCapability.resolve(data);
        });

        messageHandler.on("StartRenderPage", data => {
            if (this.destroyed) {
                return; // Ignore any pending requests if the worker was terminated.
            }

            const page = this.#pageCache.get(data.pageIndex);
            page._startRenderPage(data.transparency, data.cacheKey);
        });

        messageHandler.on("commonobj", ([id, type, exportedData]) => {
            if (this.destroyed) {
                return; // Ignore any pending requests if the worker was terminated.
            }

            if (this.commonObjs.has(id)) {
                return;
            }

            switch (type) {
                case "Font":
                    const params = this._params;

                    if ("error" in exportedData) {
                        const exportedError = exportedData.error;
                        warn(`Error during font loading: ${exportedError}`);
                        this.commonObjs.resolve(id, exportedError);
                        break;
                    }

                    let fontRegistry = null;
                    if (params.pdfBug && globalThis.FontInspector?.enabled) {
                        fontRegistry = {
                            registerFont(font, url) {
                                globalThis.FontInspector.fontAdded(font, url);
                            },
                        };
                    }
                    const font = new FontFaceObject(exportedData, {
                        isEvalSupported: params.isEvalSupported,
                        disableFontFace: params.disableFontFace,
                        ignoreErrors: params.ignoreErrors,
                        onUnsupportedFeature: this._onUnsupportedFeature.bind(this),
                        fontRegistry,
                    });
                    this.commonObjs.resolve(id, font);
                    //TODO sreeni
                    /*this.fontLoader
                        .bind(font)
                        .catch(reason => {
                            return messageHandler.sendWithPromise("FontFallback", { id });
                        })
                        .finally(() => {
                            if (!params.fontExtraProperties && font.data) {
                                // Immediately release the `font.data` property once the font
                                // has been attached to the DOM, since it's no longer needed,
                                // rather than waiting for a `PDFDocumentProxy.cleanup` call.
                                // Since `font.data` could be very large, e.g. in some cases
                                // multiple megabytes, this will help reduce memory usage.
                                font.data = null;
                            }
                            this.commonObjs.resolve(id, font);
                        });*/
                    break;
                case "FontPath":
                case "Image":
                    this.commonObjs.resolve(id, exportedData);
                    break;
                default:
                    throw new Error(`Got unknown common object type ${type}`);
            }
        });

        messageHandler.on("obj", ([id, pageIndex, type, imageData]) => {
            if (this.destroyed) {
                // Ignore any pending requests if the worker was terminated.
                return;
            }

            const pageProxy = this.#pageCache.get(pageIndex);
            if (pageProxy.objs.has(id)) {
                return;
            }

            switch (type) {
                case "Image":
                    pageProxy.objs.resolve(id, imageData);

                // Heuristic that will allow us not to store large data.
                    const MAX_IMAGE_SIZE_TO_STORE = 8000000;
                    if (imageData?.data?.length > MAX_IMAGE_SIZE_TO_STORE) {
                        pageProxy.cleanupAfterRender = true;
                    }
                    break;
                case "Pattern":
                    pageProxy.objs.resolve(id, imageData);
                    break;
                default:
                    throw new Error(`Got unknown object type ${type}`);
            }
        });

        messageHandler.on("DocProgress", data => {
            if (this.destroyed) {
                return; // Ignore any pending requests if the worker was terminated.
            }
            loadingTask.onProgress?.({
                loaded: data.loaded,
                total: data.total,
            });
        });

        messageHandler.on("DocStats", data => {
            if (this.destroyed) {
                return; // Ignore any pending requests if the worker was terminated.
            }
            // Ensure that a `PDFDocumentProxy.stats` call-site cannot accidentally
            // modify this internal data.
            this.#docStats = Object.freeze({
                streamTypes: Object.freeze(data.streamTypes),
                fontTypes: Object.freeze(data.fontTypes),
            });
        });

        messageHandler.on(
            "UnsupportedFeature",
        this._onUnsupportedFeature.bind(this)
        );

        messageHandler.on("FetchBuiltInCMap", data => {
            if (this.destroyed) {
                return Promise.reject(new Error("Worker was destroyed."));
            }
            if (!this.CMapReaderFactory) {
                return Promise.reject(
                    new Error(
                        "CMapReaderFactory not initialized, see the `useWorkerFetch` parameter."
                    )
                );
            }
            return this.CMapReaderFactory.fetch(data);
        });

        messageHandler.on("FetchStandardFontData", data => {
            if (this.destroyed) {
                return Promise.reject(new Error("Worker was destroyed."));
            }
            if (!this.StandardFontDataFactory) {
                return Promise.reject(
                    new Error(
                        "StandardFontDataFactory not initialized, see the `useWorkerFetch` parameter."
                    )
                );
            }
            return this.StandardFontDataFactory.fetch(data);
        });
    }

    _onUnsupportedFeature({ featureId }) {
        if (this.destroyed) {
            return; // Ignore any pending requests if the worker was terminated.
        }
        this.loadingTask.onUnsupportedFeature?.(featureId);
    }

    getData() {
        return this.messageHandler.sendWithPromise("GetData", null);
    }

    getPage(pageNumber) {
        if (
        !Number.isInteger(pageNumber) ||
        pageNumber <= 0 ||
        pageNumber > this._numPages
        ) {
            return Promise.reject(new Error("Invalid page request."));
        }

        const pageIndex = pageNumber - 1,
            cachedPromise = this.#pagePromises.get(pageIndex);
        if (cachedPromise) {
            return cachedPromise;
        }
        const promise = this.messageHandler
            .sendWithPromise("GetPage", {
                pageIndex,
            })
            .then(pageInfo => {
                if (this.destroyed) {
                    throw new Error("Transport destroyed");
                }
                const page = new PDFPageProxy(
                    pageIndex,
                    pageInfo,
                    this,
                    this._params.ownerDocument,
                    this._params.pdfBug
                );
                this.#pageCache.set(pageIndex, page);
                return page;
            });
        this.#pagePromises.set(pageIndex, promise);
        return promise;
    }

    getPageIndex(ref) {
        if (
        typeof ref !== "object" ||
        ref === null ||
        !Number.isInteger(ref.num) ||
        ref.num < 0 ||
        !Number.isInteger(ref.gen) ||
        ref.gen < 0
        ) {
            return Promise.reject(new Error("Invalid pageIndex request."));
        }
        return this.messageHandler.sendWithPromise("GetPageIndex", {
            num: ref.num,
            gen: ref.gen,
        });
    }

    getAnnotations(pageIndex, intent) {
        return this.messageHandler.sendWithPromise("GetAnnotations", {
            pageIndex,
            intent,
        });
    }

    saveDocument() {
        return this.messageHandler
            .sendWithPromise("SaveDocument", {
                isPureXfa: !!this._htmlForXfa,
                numPages: this._numPages,
                annotationStorage: this.annotationStorage.serializable,
                filename: this._fullReader?.filename ?? null,
            })
            .finally(() => {
                this.annotationStorage.resetModified();
            });
    }

    getFieldObjects() {
        return (this._getFieldObjectsPromise ||=
        this.messageHandler.sendWithPromise("GetFieldObjects", null));
    }

    hasJSActions() {
        return (this._hasJSActionsPromise ||= this.messageHandler.sendWithPromise(
            "HasJSActions",
            null
        ));
    }

    getCalculationOrderIds() {
        return this.messageHandler.sendWithPromise("GetCalculationOrderIds", null);
    }

    getDestinations() {
        return this.messageHandler.sendWithPromise("GetDestinations", null);
    }

    getDestination(id) {
        if (typeof id !== "string") {
            return Promise.reject(new Error("Invalid destination request."));
        }
        return this.messageHandler.sendWithPromise("GetDestination", {
            id,
        });
    }

    getPageLabels() {
        return this.messageHandler.sendWithPromise("GetPageLabels", null);
    }

    getPageLayout() {
        return this.messageHandler.sendWithPromise("GetPageLayout", null);
    }

    getPageMode() {
        return this.messageHandler.sendWithPromise("GetPageMode", null);
    }

    getViewerPreferences() {
        return this.messageHandler.sendWithPromise("GetViewerPreferences", null);
    }

    getOpenAction() {
        return this.messageHandler.sendWithPromise("GetOpenAction", null);
    }

    getAttachments() {
        return this.messageHandler.sendWithPromise("GetAttachments", null);
    }

    getJavaScript() {
        return this.messageHandler.sendWithPromise("GetJavaScript", null);
    }

    getDocJSActions() {
        return this.messageHandler.sendWithPromise("GetDocJSActions", null);
    }

    getPageJSActions(pageIndex) {
        return this.messageHandler.sendWithPromise("GetPageJSActions", {
            pageIndex,
        });
    }

    getStructTree(pageIndex) {
        return this.messageHandler.sendWithPromise("GetStructTree", {
            pageIndex,
        });
    }

    getOutline() {
        return this.messageHandler.sendWithPromise("GetOutline", null);
    }

    getOptionalContentConfig() {
        return this.messageHandler
            .sendWithPromise("GetOptionalContentConfig", null)
            .then(results => {
                return new OptionalContentConfig(results);
            });
    }

    getPermissions() {
        return this.messageHandler.sendWithPromise("GetPermissions", null);
    }

    getMetadata() {
        return (this.#metadataPromise ||= this.messageHandler
            .sendWithPromise("GetMetadata", null)
            .then(results => {
                return {
                    info: results[0],
                    metadata: results[1] ? new Metadata(results[1]) : null,
                    contentDispositionFilename: this._fullReader?.filename ?? null,
                    contentLength: this._fullReader?.contentLength ?? null,
                };
            }));
    }

    getMarkInfo() {
        return this.messageHandler.sendWithPromise("GetMarkInfo", null);
    }

    async startCleanup(keepLoadedFonts = false) {
        await this.messageHandler.sendWithPromise("Cleanup", null);

        if (this.destroyed) {
            return; // No need to manually clean-up when destruction has started.
        }
        for (const page of this.#pageCache.values()) {
            const cleanupSuccessful = page.cleanup();

            if (!cleanupSuccessful) {
                throw new Error(
                    `startCleanup: Page ${page.pageNumber} is currently rendering.`
                );
            }
        }
        this.commonObjs.clear();
        if (!keepLoadedFonts) {
            this.fontLoader.clear();
        }
        this.#metadataPromise = null;
        this._getFieldObjectsPromise = null;
        this._hasJSActionsPromise = null;
    }

    get loadingParams() {
        const params = this._params;
        return shadow(this, "loadingParams", {
            disableAutoFetch: params.disableAutoFetch,
            enableXfa: params.enableXfa,
        });
    }
}

/**
 * Proxy to a `PDFDocument` in the worker thread.
 */
class PDFDocumentProxy {
    constructor(pdfInfo, transport) {
        this._pdfInfo = pdfInfo;
        this._transport = transport;
    }

    get annotationStorage() {
        return this._transport.annotationStorage;
    }

    /**
     * @type {number} Total number of pages in the PDF file.
     */
    get numPages() {
        return this._pdfInfo.numPages;
    }

    /**
     * @type {Array<string, string|null>} A (not guaranteed to be) unique ID to
     *   identify the PDF document.
     *   NOTE: The first element will always be defined for all PDF documents,
     *   whereas the second element is only defined for *modified* PDF documents.
     */
    get fingerprints() {
        return this._pdfInfo.fingerprints;
    }

    /**
     * @typedef {Object} PDFDocumentStats
     * @property {Object<string, boolean>} streamTypes - Used stream types in the
     *   document (an item is set to true if specific stream ID was used in the
     *   document).
     * @property {Object<string, boolean>} fontTypes - Used font types in the
     *   document (an item is set to true if specific font ID was used in the
     *   document).
     */

    /**
     * @type {PDFDocumentStats | null} The current statistics about document
     *   structures, or `null` when no statistics exists.
     */
    get stats() {
        return this._transport.stats;
    }

    /**
     * @type {boolean} True if only XFA form.
     */
    get isPureXfa() {
        return !!this._transport._htmlForXfa;
    }

    /**
     * NOTE: This is (mostly) intended to support printing of XFA forms.
     *
     * @type {Object | null} An object representing a HTML tree structure
     *   to render the XFA, or `null` when no XFA form exists.
     */
    get allXfaHtml() {
        return this._transport._htmlForXfa;
    }

    /**
     * @param {number} pageNumber - The page number to get. The first page is 1.
     * @returns {Promise<PDFPageProxy>} A promise that is resolved with
     *   a {@link PDFPageProxy} object.
     */
    getPage(pageNumber) {
        return this._transport.getPage(pageNumber);
    }

    getPageIndex(ref) {
        return this._transport.getPageIndex(ref);
    }

    getDestinations() {
        return this._transport.getDestinations();
    }

    getDestination(id) {
        return this._transport.getDestination(id);
    }

    getPageLabels() {
        return this._transport.getPageLabels();
    }

    getPageLayout() {
        return this._transport.getPageLayout();
    }

    getPageMode() {
        return this._transport.getPageMode();
    }

    getViewerPreferences() {
        return this._transport.getViewerPreferences();
    }

    getOpenAction() {
        return this._transport.getOpenAction();
    }

    getAttachments() {
        return this._transport.getAttachments();
    }

    /**
     * @returns {Promise<Array<string> | null>} A promise that is resolved with
     *   an {Array} of all the JavaScript strings in the name tree, or `null`
     *   if no JavaScript exists.
     */
    getJavaScript() {
        return this._transport.getJavaScript();
    }

    /**
     * @returns {Promise<Object | null>} A promise that is resolved with
     *   an {Object} with the JavaScript actions:
     *     - from the name tree (like getJavaScript);
     *     - from A or AA entries in the catalog dictionary.
     *   , or `null` if no JavaScript exists.
     */
    getJSActions() {
        return this._transport.getDocJSActions();
    }

    /**
     * @typedef {Object} OutlineNode
     * @property {string} title
     * @property {boolean} bold
     * @property {boolean} italic
     * @property {Uint8ClampedArray} color - The color in RGB format to use for
     *   display purposes.
     * @property {string | Array<any> | null} dest
     * @property {string | null} url
     * @property {string | undefined} unsafeUrl
     * @property {boolean | undefined} newWindow
     * @property {number | undefined} count
     * @property {Array<OutlineNode>} items
     */

    /**
     * @returns {Promise<Array<OutlineNode>>} A promise that is resolved with an
     *   {Array} that is a tree outline (if it has one) of the PDF file.
     */
    getOutline() {
        return this._transport.getOutline();
    }

    getOptionalContentConfig() {
        return this._transport.getOptionalContentConfig();
    }

    /**
     * @returns {Promise<Array<number> | null>} A promise that is resolved with
     *   an {Array} that contains the permission flags for the PDF document, or
     *   `null` when no permissions are present in the PDF file.
     */
    getPermissions() {
        return this._transport.getPermissions();
    }

    /**
     * @returns {Promise<{ info: Object, metadata: Metadata }>} A promise that is
     *   resolved with an {Object} that has `info` and `metadata` properties.
     *   `info` is an {Object} filled with anything available in the information
     *   dictionary and similarly `metadata` is a {Metadata} object with
     *   information from the metadata section of the PDF.
     */
    getMetadata() {
        return this._transport.getMetadata();
    }

    /**
     * @typedef {Object} MarkInfo
     * Properties correspond to Table 321 of the PDF 32000-1:2008 spec.
     * @property {boolean} Marked
     * @property {boolean} UserProperties
     * @property {boolean} Suspects
     */

    /**
     * @returns {Promise<MarkInfo | null>} A promise that is resolved with
     *   a {MarkInfo} object that contains the MarkInfo flags for the PDF
     *   document, or `null` when no MarkInfo values are present in the PDF file.
     */
    getMarkInfo() {
        return this._transport.getMarkInfo();
    }

    getData() {
        return this._transport.getData();
    }

    /**
     * @returns {Promise<{ length: number }>} A promise that is resolved when the
     *   document's data is loaded. It is resolved with an {Object} that contains
     *   the `length` property that indicates size of the PDF data in bytes.
     */
    getDownloadInfo() {
        return this._transport.downloadInfoCapability.promise;
    }

    /**
     * Cleans up resources allocated by the document on both the main and worker
     * threads.
     *
     * NOTE: Do not, under any circumstances, call this method when rendering is
     * currently ongoing since that may lead to rendering errors.
     *
     * @param {boolean} [keepLoadedFonts] - Let fonts remain attached to the DOM.
     *   NOTE: This will increase persistent memory usage, hence don't use this
     *   option unless absolutely necessary. The default value is `false`.
     * @returns {Promise} A promise that is resolved when clean-up has finished.
     */
    cleanup(keepLoadedFonts = false) {
        return this._transport.startCleanup(keepLoadedFonts || this.isPureXfa);
    }

    /**
     * Destroys the current document instance and terminates the worker.
     */
    destroy() {
        return this.loadingTask.destroy();
    }

    get loadingParams() {
        return this._transport.loadingParams;
    }

    /**
     * @type {PDFDocumentLoadingTask} The loadingTask for the current document.
     */
    get loadingTask() {
        return this._transport.loadingTask;
    }

    /**
     * @returns {Promise<Uint8Array>} A promise that is resolved with a
     *   {Uint8Array} containing the full data of the saved document.
     */
    saveDocument() {
        return this._transport.saveDocument();
    }

    /**
     * @returns {Promise<Object<string, Array<Object>> | null>} A promise that is
     *   resolved with an {Object} containing /AcroForm field data for the JS
     *   sandbox, or `null` when no field data is present in the PDF file.
     */
    getFieldObjects() {
        return this._transport.getFieldObjects();
    }

    /**
     * @returns {Promise<boolean>} A promise that is resolved with `true`
     *   if some /AcroForm fields have JavaScript actions.
     */
    hasJSActions() {
        return this._transport.hasJSActions();
    }

    /**
     * @returns {Promise<Array<string> | null>} A promise that is resolved with an
     *   {Array<string>} containing IDs of annotations that have a calculation
     *   action, or `null` when no such annotations are present in the PDF file.
     */
    getCalculationOrderIds() {
        return this._transport.getCalculationOrderIds();
    }
}

class PDFObjects {
    #objs = Object.create(null);

    /**
     * Ensures there is an object defined for `objId`.
     *
     * @param {string} objId
     * @returns {Object}
     */
    ensureObj(objId) {
        const obj = this.#objs[objId];
        if (obj) {
            return obj;
        }
        return (this.#objs[objId] = {
            capability: createPromiseCapability(),
            data: null,
        });
    }

    /**
     * If called *without* callback, this returns the data of `objId` but the
     * object needs to be resolved. If it isn't, this method throws.
     *
     * If called *with* a callback, the callback is called with the data of the
     * object once the object is resolved. That means, if you call this method
     * and the object is already resolved, the callback gets called right away.
     *
     * @param {string} objId
     * @param {function} [callback]
     * @returns {any}
     */
    get(objId, callback = null) {
        // If there is a callback, then the get can be async and the object is
        // not required to be resolved right now.
        if (callback) {
            const obj = this.ensureObj(objId);
            obj.capability.promise.then(() => callback(obj.data));
            return null;
        }
        // If there isn't a callback, the user expects to get the resolved data
        // directly.
        const obj = this.#objs[objId];
        // If there isn't an object yet or the object isn't resolved, then the
        // data isn't ready yet!
        if (!obj?.capability.settled) {
            throw new Error(`Requesting object that isn't resolved yet ${objId}.`);
        }
        return obj.data;
    }

    /**
     * @param {string} objId
     * @returns {boolean}
     */
    has(objId) {
        const obj = this.#objs[objId];
        return obj?.capability.settled || false;
    }

    /**
     * Resolves the object `objId` with optional `data`.
     *
     * @param {string} objId
     * @param {any} [data]
     */
    resolve(objId, data = null) {
        const obj = this.ensureObj(objId);
        obj.data = data;
        obj.capability.resolve();
    }

    clear() {
        this.#objs = Object.create(null);
    }
}

class PDFPageProxy {
    constructor(pageIndex, pageInfo, transport, ownerDocument, pdfBug = false) {
        this._pageIndex = pageIndex;
        this._pageInfo = pageInfo;
        this._ownerDocument = ownerDocument;
        this._transport = transport;
        //TODO sreeni
        //this._stats = pdfBug ? new StatTimer() : null;
        this._stats = null;
        this._pdfBug = pdfBug;
        this.commonObjs = transport.commonObjs;
        this.objs = new PDFObjects();

        this.cleanupAfterRender = false;
        this.pendingCleanup = false;
        this._intentStates = new Map();
        this._annotationPromises = new Map();
        this.destroyed = false;
    }

    /**
     * @type {number} Page number of the page. First page is 1.
     */
    get pageNumber() {
        return this._pageIndex + 1;
    }

    /**
     * @type {number} The number of degrees the page is rotated clockwise.
     */
    get rotate() {
        return this._pageInfo.rotate;
    }

    /**
     * @type {RefProxy | null} The reference that points to this page.
     */
    get ref() {
        return this._pageInfo.ref;
    }

    /**
     * @type {number} The default size of units in 1/72nds of an inch.
     */
    get userUnit() {
        return this._pageInfo.userUnit;
    }

    /**
     * @type {Array<number>} An array of the visible portion of the PDF page in
     *   user space units [x1, y1, x2, y2].
     */
    get view() {
        return this._pageInfo.view;
    }

    getViewport({
                    scale,
                    rotation = this.rotate,
                    offsetX = 0,
                    offsetY = 0,
                    dontFlip = false,
                } = {}) {
        return new PageViewport({
            viewBox: this.view,
            scale,
            rotation,
            offsetX,
            offsetY,
            dontFlip,
        });
    }

    getAnnotations({ intent = "display" } = {}) {
        const intentArgs = this._transport.getRenderingIntent(intent);

        let promise = this._annotationPromises.get(intentArgs.cacheKey);
        if (!promise) {
            promise = this._transport.getAnnotations(
                this._pageIndex,
                intentArgs.renderingIntent
            );
            this._annotationPromises.set(intentArgs.cacheKey, promise);
        }
        return promise;
    }

    /**
     * @returns {Promise<Object>} A promise that is resolved with an
     *   {Object} with JS actions.
     */
    getJSActions() {
        return (this._jsActionsPromise ||= this._transport.getPageJSActions(
            this._pageIndex
        ));
    }

    /**
     * @returns {Promise<Object | null>} A promise that is resolved with
     *   an {Object} with a fake DOM object (a tree structure where elements
     *   are {Object} with a name, attributes (class, style, ...), value and
     *   children, very similar to a HTML DOM tree), or `null` if no XFA exists.
     */
    async getXfa() {
        return this._transport._htmlForXfa?.children[this._pageIndex] || null;
    }

    render({
               canvasContext,
               viewport,
               intent = "display",
               annotationMode = AnnotationMode.ENABLE,
               transform = null,
               imageLayer = null,
               canvasFactory = null,
               background = null,
               optionalContentConfigPromise = null,
               annotationCanvasMap = null,
           }) {

        if (this._stats) {
            this._stats.time("Overall");
        }

        const intentArgs = this._transport.getRenderingIntent(
            intent,
            annotationMode
        );
        // If there was a pending destroy, cancel it so no cleanup happens during
        // this call to render.
        this.pendingCleanup = false;

        if (!optionalContentConfigPromise) {
            optionalContentConfigPromise = this._transport.getOptionalContentConfig();
        }

        let intentState = this._intentStates.get(intentArgs.cacheKey);
        if (!intentState) {
            intentState = Object.create(null);
            this._intentStates.set(intentArgs.cacheKey, intentState);
        }

        // Ensure that a pending `streamReader` cancel timeout is always aborted.
        if (intentState.streamReaderCancelTimeout) {
            clearTimeout(intentState.streamReaderCancelTimeout);
            intentState.streamReaderCancelTimeout = null;
        }

        //TODO sreni
        const canvasFactoryInstance =
            canvasFactory ||
            new DefaultCanvasFactory({ ownerDocument: this._ownerDocument });
        //const canvasFactoryInstance = null;
        const intentPrint = !!(
            intentArgs.renderingIntent & RenderingIntentFlag.PRINT
        );

        // If there's no displayReadyCapability yet, then the operatorList
        // was never requested before. Make the request and create the promise.
        if (!intentState.displayReadyCapability) {
            intentState.displayReadyCapability = createPromiseCapability();
            intentState.operatorList = {
                fnArray: [],
                argsArray: [],
                lastChunk: false,
            };

            if (this._stats) {
                this._stats.time("Page Request");
            }
            this._pumpOperatorList(intentArgs);
        }

        const complete = error => {
            intentState.renderTasks.delete(internalRenderTask);

            // Attempt to reduce memory usage during *printing*, by always running
            // cleanup once rendering has finished (regardless of cleanupAfterRender).
            if (this.cleanupAfterRender || intentPrint) {
                this.pendingCleanup = true;
            }
            this._tryCleanup();

            if (error) {
                internalRenderTask.capability.reject(error);

                this._abortOperatorList({
                    intentState,
                    reason: error instanceof Error ? error : new Error(error),
                });
            } else {
                internalRenderTask.capability.resolve();
            }
            if (this._stats) {
                this._stats.timeEnd("Rendering");
                this._stats.timeEnd("Overall");
            }
        };

        const internalRenderTask = new InternalRenderTask({
            callback: complete,
            // Only include the required properties, and *not* the entire object.
            params: {
                canvasContext,
                viewport,
                transform,
                imageLayer,
                background,
            },
            objs: this.objs,
            commonObjs: this.commonObjs,
            annotationCanvasMap,
            operatorList: intentState.operatorList,
            pageIndex: this._pageIndex,
            canvasFactory: canvasFactoryInstance,
            useRequestAnimationFrame: !intentPrint,
            pdfBug: this._pdfBug,
        });

        (intentState.renderTasks ||= new Set()).add(internalRenderTask);
        const renderTask = internalRenderTask.task;

        Promise.all([
            intentState.displayReadyCapability.promise,
            optionalContentConfigPromise,
        ])
            .then(([transparency, optionalContentConfig]) => {
                if (this.pendingCleanup) {
                    complete();
                    return;
                }
                if (this._stats) {
                    this._stats.time("Rendering");
                }
                internalRenderTask.initializeGraphics({
                    transparency,
                    optionalContentConfig,
                });
                internalRenderTask.operatorListChanged();
            })
            .catch(complete);

        return renderTask;
    }

    getOperatorList({
                        intent = "display",
                        annotationMode = AnnotationMode.ENABLE,
                    } = {}) {
        function operatorListChanged() {
            if (intentState.operatorList.lastChunk) {
                intentState.opListReadCapability.resolve(intentState.operatorList);

                intentState.renderTasks.delete(opListTask);
            }
        }

        const intentArgs = this._transport.getRenderingIntent(
            intent,
            annotationMode,
            /* isOpList = */ true
        );
        let intentState = this._intentStates.get(intentArgs.cacheKey);
        if (!intentState) {
            intentState = Object.create(null);
            this._intentStates.set(intentArgs.cacheKey, intentState);
        }
        let opListTask;

        if (!intentState.opListReadCapability) {
            opListTask = Object.create(null);
            opListTask.operatorListChanged = operatorListChanged;
            intentState.opListReadCapability = createPromiseCapability();
            (intentState.renderTasks ||= new Set()).add(opListTask);
            intentState.operatorList = {
                fnArray: [],
                argsArray: [],
                lastChunk: false,
            };

            if (this._stats) {
                this._stats.time("Page Request");
            }
            this._pumpOperatorList(intentArgs);
        }
        return intentState.opListReadCapability.promise;
    }


    streamTextContent({
                          disableCombineTextItems = false,
                          includeMarkedContent = false,
                      } = {}) {
        const TEXT_CONTENT_CHUNK_SIZE = 100;

        return this._transport.messageHandler.sendWithStream(
            "GetTextContent",
            {
                pageIndex: this._pageIndex,
                combineTextItems: disableCombineTextItems !== true,
                includeMarkedContent: includeMarkedContent === true,
            },
            {
                highWaterMark: TEXT_CONTENT_CHUNK_SIZE,
                size(textContent) {
                    return textContent.items.length;
                },
            }
        );
    }

    getTextContent(params = {}) {
        if (this._transport._htmlForXfa) {
            // TODO: We need to revisit this once the XFA foreground patch lands and
            // only do this for non-foreground XFA.
            return this.getXfa().then(xfa => {
                return XfaText.textContent(xfa);
            });
        }
        const readableStream = this.streamTextContent(params);

        return new Promise(function (resolve, reject) {
            function pump() {
                reader.read().then(function ({ value, done }) {
                    if (done) {
                        resolve(textContent);
                        return;
                    }
                    Object.assign(textContent.styles, value.styles);
                    textContent.items.push(...value.items);
                    pump();
                }, reject);
            }

            const reader = readableStream.getReader();
            const textContent = {
                items: [],
                styles: Object.create(null),
            };
            pump();
        });
    }

    getStructTree() {
        return (this._structTreePromise ||= this._transport.getStructTree(
            this._pageIndex
        ));
    }

    /**
     * Destroys the page object.
     * @private
     */
    _destroy() {
        this.destroyed = true;

        const waitOn = [];
        for (const intentState of this._intentStates.values()) {
            this._abortOperatorList({
                intentState,
                reason: new Error("Page was destroyed."),
                force: true,
            });

            if (intentState.opListReadCapability) {
                // Avoid errors below, since the renderTasks are just stubs.
                continue;
            }
            for (const internalRenderTask of intentState.renderTasks) {
                waitOn.push(internalRenderTask.completed);
                internalRenderTask.cancel();
            }
        }
        this.objs.clear();
        this._annotationPromises.clear();
        this._jsActionsPromise = null;
        this._structTreePromise = null;
        this.pendingCleanup = false;
        return Promise.all(waitOn);
    }

    /**
     * Cleans up resources allocated by the page.
     *
     * @param {boolean} [resetStats] - Reset page stats, if enabled.
     *   The default value is `false`.
     * @returns {boolean} Indicates if clean-up was successfully run.
     */
    cleanup(resetStats = false) {
        this.pendingCleanup = true;
        return this._tryCleanup(resetStats);
    }

    /**
     * Attempts to clean up if rendering is in a state where that's possible.
     * @private
     */
    _tryCleanup(resetStats = false) {
        if (!this.pendingCleanup) {
            return false;
        }
        for (const { renderTasks, operatorList } of this._intentStates.values()) {
            if (renderTasks.size > 0 || !operatorList.lastChunk) {
                return false;
            }
        }

        this._intentStates.clear();
        this.objs.clear();
        this._annotationPromises.clear();
        this._jsActionsPromise = null;
        this._structTreePromise = null;
        if (resetStats && this._stats) {
            //TODO sreeni
            //this._stats = new StatTimer();
        }
        this.pendingCleanup = false;
        return true;
    }

    /**
     * @private
     */
    _startRenderPage(transparency, cacheKey) {
        const intentState = this._intentStates.get(cacheKey);
        if (!intentState) {
            return; // Rendering was cancelled.
        }
        if (this._stats) {
            this._stats.timeEnd("Page Request");
        }
        // TODO Refactor RenderPageRequest to separate rendering
        // and operator list logic
        if (intentState.displayReadyCapability) {
            intentState.displayReadyCapability.resolve(transparency);
        }
    }

    /**
     * @private
     */
    _renderPageChunk(operatorListChunk, intentState) {
        // Add the new chunk to the current operator list.
        for (let i = 0, ii = operatorListChunk.length; i < ii; i++) {
            intentState.operatorList.fnArray.push(operatorListChunk.fnArray[i]);
            intentState.operatorList.argsArray.push(operatorListChunk.argsArray[i]);
        }
        intentState.operatorList.lastChunk = operatorListChunk.lastChunk;

        // Notify all the rendering tasks there are more operators to be consumed.
        for (const internalRenderTask of intentState.renderTasks) {
            internalRenderTask.operatorListChanged();
        }

        if (operatorListChunk.lastChunk) {
            this._tryCleanup();
        }
    }

    /**
     * @private
     */
    _pumpOperatorList({ renderingIntent, cacheKey }) {
        const readableStream = this._transport.messageHandler.sendWithStream(
            "GetOperatorList",
            {
                pageIndex: this._pageIndex,
                intent: renderingIntent,
                cacheKey,
                annotationStorage:
                    renderingIntent & RenderingIntentFlag.ANNOTATIONS_STORAGE
                    ? this._transport.annotationStorage.serializable
                    : null,
            }
        );
        const reader = readableStream.getReader();

        const intentState = this._intentStates.get(cacheKey);
        intentState.streamReader = reader;

        const pump = () => {
            reader.read().then(
                (value) => {
                    if(value.done){
                        intentState.streamReader = null;
                        return;
                    }

                    if (this._transport.destroyed) {
                        return; // Ignore any pending requests if the worker was terminated.
                    }
                    this._renderPageChunk(value.data, intentState);
                    pump();
                }
            );
            /*reader.read().then(
                ({ value, done }) => {
                    if (done) {
                        intentState.streamReader = null;
                        return;
                    }
                    if (this._transport.destroyed) {
                        return; // Ignore any pending requests if the worker was terminated.
                    }
                    this._renderPageChunk(value, intentState);
                    pump();
                },
                reason => {
                    intentState.streamReader = null;

                    if (this._transport.destroyed) {
                        return; // Ignore any pending requests if the worker was terminated.
                    }
                    if (intentState.operatorList) {
                        // Mark operator list as complete.
                        intentState.operatorList.lastChunk = true;

                        for (const internalRenderTask of intentState.renderTasks) {
                            internalRenderTask.operatorListChanged();
                        }
                        this._tryCleanup();
                    }

                    if (intentState.displayReadyCapability) {
                        intentState.displayReadyCapability.reject(reason);
                    } else if (intentState.opListReadCapability) {
                        intentState.opListReadCapability.reject(reason);
                    } else {
                        throw reason;
                    }
                }
            );*/
        };
        pump();
    }

    /**
     * @private
     */
    _abortOperatorList({ intentState, reason, force = false }) {

        if (!intentState.streamReader) {
            return;
        }
        if (!force) {
            // Ensure that an Error occurring in *only* one `InternalRenderTask`, e.g.
            // multiple render() calls on the same canvas, won't break all rendering.
            if (intentState.renderTasks.size > 0) {
                return;
            }
            // Don't immediately abort parsing on the worker-thread when rendering is
            // cancelled, since that will unnecessarily delay re-rendering when (for
            // partially parsed pages) e.g. zooming/rotation occurs in the viewer.
            //TODO sreeni
            /*if (reason instanceof RenderingCancelledException) {
                intentState.streamReaderCancelTimeout = setTimeout(() => {
                    this._abortOperatorList({ intentState, reason, force: true });
                    intentState.streamReaderCancelTimeout = null;
                }, RENDERING_CANCELLED_TIMEOUT);
                return;
            }*/
        }
        intentState.streamReader
            .cancel(new AbortException(reason.message))
            .catch(() => {
                // Avoid "Uncaught promise" messages in the console.
            });
        intentState.streamReader = null;

        if (this._transport.destroyed) {
            return; // Ignore any pending requests if the worker was terminated.
        }
        // Remove the current `intentState`, since a cancelled `getOperatorList`
        // call on the worker-thread cannot be re-started...
        for (const [curCacheKey, curIntentState] of this._intentStates) {
            if (curIntentState === intentState) {
                this._intentStates.delete(curCacheKey);
                break;
            }
        }
        // ... and force clean-up to ensure that any old state is always removed.
        this.cleanup();
    }

    /**
     * @type {Object} Returns page stats, if enabled; returns `null` otherwise.
     */
    get stats() {
        return this._stats;
    }
}

/**
 * Allows controlling of the rendering tasks.
 */
class RenderTask {
    constructor(internalRenderTask) {
        this._internalRenderTask = internalRenderTask;

        /**
         * Callback for incremental rendering -- a function that will be called
         * each time the rendering is paused.  To continue rendering call the
         * function that is the first argument to the callback.
         * @type {function}
         */
        this.onContinue = null;
    }

    /**
     * Promise for rendering task completion.
     * @type {Promise<void>}
     */
    get promise() {
        return this._internalRenderTask.capability.promise;
    }

    /**
     * Cancels the rendering task. If the task is currently rendering it will
     * not be cancelled until graphics pauses with a timeout. The promise that
     * this object extends will be rejected when cancelled.
     */
    cancel() {
        this._internalRenderTask.cancel();
    }
}

class InternalRenderTask {
    static get canvasInUse() {
        return shadow(this, "canvasInUse", new WeakSet());
    }

    constructor({
                    callback,
                    params,
                    objs,
                    commonObjs,
                    annotationCanvasMap,
                    operatorList,
                    pageIndex,
                    canvasFactory,
                    useRequestAnimationFrame = false,
                    pdfBug = false,
                }) {
        this.callback = callback;
        this.params = params;
        this.objs = objs;
        this.commonObjs = commonObjs;
        this.annotationCanvasMap = annotationCanvasMap;
        this.operatorListIdx = null;
        this.operatorList = operatorList;
        this._pageIndex = pageIndex;
        this.canvasFactory = canvasFactory;
        this._pdfBug = pdfBug;

        this.running = false;
        this.graphicsReadyCallback = null;
        this.graphicsReady = false;
        this._useRequestAnimationFrame = false;
        this.cancelled = false;
        this.capability = createPromiseCapability();
        this.task = new RenderTask(this);
        // caching this-bound methods
        this._cancelBound = this.cancel.bind(this);
        this._continueBound = this._continue.bind(this);
        this._scheduleNextBound = this._scheduleNext.bind(this);
        this._nextBound = this._next.bind(this);
        this._canvas = params.canvasContext.canvas;
    }

    get completed() {
        return this.capability.promise.catch(function () {
            // Ignoring errors, since we only want to know when rendering is
            // no longer pending.
        });
    }

    initializeGraphics({ transparency = false, optionalContentConfig }) {
        if (this.cancelled) {
            return;
        }
        if (this._canvas) {
            if (InternalRenderTask.canvasInUse.has(this._canvas)) {
                throw new Error(
                    "Cannot use the same canvas during multiple render() operations. " +
                    "Use different canvas or ensure previous operations were " +
                    "cancelled or completed."
                );
            }
            InternalRenderTask.canvasInUse.add(this._canvas);
        }

        if (this._pdfBug && globalThis.StepperManager?.enabled) {
            this.stepper = globalThis.StepperManager.create(this._pageIndex);
            this.stepper.init(this.operatorList);
            this.stepper.nextBreakPoint = this.stepper.getNextBreakPoint();
        }
        const { canvasContext, viewport, transform, imageLayer, background } =
            this.params;

        this.gfx = new CanvasGraphics(
            canvasContext,
            this.commonObjs,
            this.objs,
            this.canvasFactory,
            imageLayer,
            optionalContentConfig,
            this.annotationCanvasMap
        );
        this.gfx.beginDrawing({
            transform,
            viewport,
            transparency,
            background,
        });
        this.operatorListIdx = 0;
        this.graphicsReady = true;
        if (this.graphicsReadyCallback) {
            this.graphicsReadyCallback();
        }
    }

    cancel(error = null) {
        this.running = false;
        this.cancelled = true;
        if (this.gfx) {
            this.gfx.endDrawing();
        }
        if (this._canvas) {
            InternalRenderTask.canvasInUse.delete(this._canvas);
        }
        this.callback(
            error ||
            new RenderingCancelledException(
                `Rendering cancelled, page ${this._pageIndex + 1}`,
                "canvas"
            )
        );
    }

    operatorListChanged() {
        if (!this.graphicsReady) {
            if (!this.graphicsReadyCallback) {
                this.graphicsReadyCallback = this._continueBound;
            }
            return;
        }

        if (this.stepper) {
            this.stepper.updateOperatorList(this.operatorList);
        }

        if (this.running) {
            return;
        }
        this._continue();
    }

    _continue() {
        this.running = true;
        if (this.cancelled) {
            return;
        }
        if (this.task.onContinue) {
            this.task.onContinue(this._scheduleNextBound);
        } else {
            this._scheduleNext();
        }
    }

    _scheduleNext() {
            Promise.resolve().then(this._nextBound).catch(this._cancelBound);
    }

    async _next() {
        if (this.cancelled) {
            return;
        }
        this.operatorListIdx = this.gfx.executeOperatorList(
            this.operatorList,
            this.operatorListIdx,
            this._continueBound,
            this.stepper
        );
        if (this.operatorListIdx === this.operatorList.argsArray.length) {
            this.running = false;
            if (this.operatorList.lastChunk) {
                this.gfx.endDrawing();
                if (this._canvas) {
                    InternalRenderTask.canvasInUse.delete(this._canvas);
                }
                this.callback();
            }
        }
    }
}

export {PDFWorker, getDocument};